Skip to content

Add presigned URL APIs#15

Merged
steven-passynkov merged 5 commits into
mainfrom
add-presigned-url-support
Apr 14, 2026
Merged

Add presigned URL APIs#15
steven-passynkov merged 5 commits into
mainfrom
add-presigned-url-support

Conversation

@steven-passynkov

@steven-passynkov steven-passynkov commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add sync and async sandbox APIs for creating and deleting presigned URLs
  • add presigned URL request and response models plus package exports
  • document the feature in the README and cover it with sandbox tests

Summary by CodeRabbit

  • New Features

    • Added presigned URL support to create temporary public URLs for sandbox ports with optional expiry and revocation.
    • SSH credentials now support multiple concurrent keys per sandbox and ID-targeted validate/regenerate/delete operations.
  • Documentation

    • Added "Presigned URLs" section with examples for creating, printing, and revoking URLs; updated SSH example to show regenerating credentials.
  • Tests

    • Added sync/async and model tests covering presigned URL and SSH behaviors.

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@steven-passynkov has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 4 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 4 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f126a21a-cb91-4fe4-b3a1-5af55a41631b

📥 Commits

Reviewing files that changed from the base of the PR and between 4b912fc and 305595f.

📒 Files selected for processing (1)
  • tests/models/test_sandbox.py
📝 Walkthrough

Walkthrough

Adds presigned URL support for sandbox ports (models, schemas, sync/async client methods, validation, error interception), updates SSH client methods to target per-credential IDs, updates README and examples, and adds tests for presigned URLs and SSH ID-based operations.

Changes

Cohort / File(s) Summary
Models & Schemas
leap0/models/sandbox.py, leap0/_schemas/sandbox.py
Added CreatePresignedURLParams (Pydantic) and PresignedURL (dataclass) plus TypedDicts CreatePresignedURLRequestDict and PresignedURLResponseDict. Runtime and input validation for port/expires_in; PresignedURL.from_dict() with redacted __repr__.
Async Sandbox
leap0/_async/sandbox.py
Added AsyncSandbox.create_presigned_url / delete_presigned_url and AsyncSandboxesClient counterparts. Implements POST /v1/sandbox/{id}/presigned-url (201) and DELETE /v1/sandbox/{id}/presigned-url/{id} (204), uses CreatePresignedURLParams, input validation, and @intercept_errors.
Sync Sandbox
leap0/_sync/sandbox.py
Added Sandbox.create_presigned_url / delete_presigned_url and SandboxesClient counterparts mirroring async flows, payload serialization, response parsing, validation, and error interception.
SSH Client Changes
leap0/_async/ssh.py, leap0/_sync/ssh.py
SSH APIs moved from implicit single-credential to explicit credential IDs: delete_access, validate_access, and regenerate_access now accept id: str (keyword-only) and call ID-targeted endpoints (/ssh/{id}, /ssh/{id}/validate, /ssh/{id}/regen), request payloads adjusted accordingly.
Module Exports & Examples
leap0/__init__.py, examples/ssh.py, README.md
Exported CreatePresignedURLParams and PresignedURL for lazy import; example ssh.py updated to use id= and call regenerate_access(id=...); README gained a "Presigned URLs" usage section.
Tests
tests/_async/test_sandboxes.py, tests/_sync/test_sandboxes.py, tests/models/test_sandbox.py, tests/_async/test_ssh.py, tests/_sync/test_ssh.py
Added tests covering presigned URL create/delete (sync & async), helper delegation, model validation and repr redaction; updated SSH tests to call/delete by credential id and assert new request paths.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Sandbox as "Sandbox (helper)"
    participant SandboxesClient as "SandboxesClient"
    participant API as "API Server"

    Client->>Sandbox: create_presigned_url(port, expires_in)
    Sandbox->>SandboxesClient: create_presigned_url(sandbox_ref, port, expires_in)
    SandboxesClient->>SandboxesClient: CreatePresignedURLParams -> to_payload()
    SandboxesClient->>API: POST /v1/sandbox/{id}/presigned-url {port, expires_in}
    API-->>SandboxesClient: 201 Created {id, token, url, port, expires_at, ...}
    SandboxesClient->>SandboxesClient: PresignedURL.from_dict(response)
    SandboxesClient-->>Sandbox: PresignedURL
    Sandbox-->>Client: PresignedURL

    Client->>Sandbox: delete_presigned_url(presigned_id)
    Sandbox->>SandboxesClient: delete_presigned_url(sandbox_ref, presigned_id)
    SandboxesClient->>SandboxesClient: validate presigned_id
    SandboxesClient->>API: DELETE /v1/sandbox/{id}/presigned-url/{presigned_id}
    API-->>SandboxesClient: 204 No Content
    SandboxesClient-->>Sandbox: None
    Sandbox-->>Client: None
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through code and found a door,
A presigned path to ports once more.
Create a token, share a line,
Then snip it back when it's past its time.
Tiny paws, tidy logs—hoppy deploys galore!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add presigned URL APIs' accurately summarizes the main change: introducing presigned URL creation and deletion functionality across sync and async sandbox APIs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-presigned-url-support

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/_sync/test_sandboxes.py (1)

112-115: Assert the 201 response contract in this create test.

Line 112 only checks method/path/body today. If expected_status=201 gets dropped from the client call, this test still passes and the regression slips through. Please assert it here and mirror the same assertion in the async twin.

Suggested test assertion
         args, kwargs = mock_transport.request_json.call_args
         assert args == ("POST", "/v1/sandbox/sbx-1/presigned-url")
         assert kwargs["json"] == {"port": 8080, "expires_in": 900}
+        assert kwargs["expected_status"] == 201
         assert result.token == "tok_1"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/_sync/test_sandboxes.py` around lines 112 - 115, The test currently
asserts method/path/body and result.token but does not assert the
expected_status contract, so update the assertion on
mock_transport.request_json.call_args to also verify kwargs["expected_status"]
== 201 (i.e., ensure the client call included expected_status=201); then apply
the same addition to the async counterpart test (the async version that also
calls mock_transport.request_json) so both sync and async tests assert
expected_status explicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@leap0/models/sandbox.py`:
- Around line 291-310: The from_dict method currently defaults missing
expires_at/created_at to empty strings which masks malformed responses; update
PresignedURL.from_dict to validate that data["expires_at"] and
data["created_at"] are present and are non-empty strings (same style as the
earlier checks for id/token/url/host/sandbox_id) and raise a ValueError if they
are missing or invalid before calling cls(...) so the model construction fails
fast on bad API responses.
- Around line 277-288: The default dataclass-generated __repr__ for PresignedURL
leaks sensitive fields (token and url); implement a custom __repr__ (or __str__)
on the PresignedURL dataclass that returns a string containing non-sensitive
fields (e.g., id, host, sandbox_id, port, expires_at, created_at) while
replacing token and url with a fixed redaction marker like "<redacted>" so any
logging or debugging won't expose credentials; keep the class as
`@dataclass`(slots=True) and ensure the method name is __repr__ in the
PresignedURL class to override the default.

---

Nitpick comments:
In `@tests/_sync/test_sandboxes.py`:
- Around line 112-115: The test currently asserts method/path/body and
result.token but does not assert the expected_status contract, so update the
assertion on mock_transport.request_json.call_args to also verify
kwargs["expected_status"] == 201 (i.e., ensure the client call included
expected_status=201); then apply the same addition to the async counterpart test
(the async version that also calls mock_transport.request_json) so both sync and
async tests assert expected_status explicitly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea7c2b08-883a-41f7-8761-82453becdf4e

📥 Commits

Reviewing files that changed from the base of the PR and between 236320f and 09c028f.

📒 Files selected for processing (9)
  • README.md
  • leap0/__init__.py
  • leap0/_async/sandbox.py
  • leap0/_schemas/sandbox.py
  • leap0/_sync/sandbox.py
  • leap0/models/sandbox.py
  • tests/_async/test_sandboxes.py
  • tests/_sync/test_sandboxes.py
  • tests/models/test_sandbox.py

Comment thread leap0/models/sandbox.py
Comment thread leap0/models/sandbox.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@leap0/models/sandbox.py`:
- Around line 310-316: The port validation in the PresignedURL response parsing
only checks type; update the logic in the from_dict (or the function/method
containing the shown code) to enforce a TCP port range by verifying that port is
an int and 1 <= port <= 65535, and raise a ValueError that includes the invalid
port value (similar style to the existing error messages) when the check fails;
leave the existing timestamp checks for expires_at/created_at unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc8657ed-a717-47b0-8fae-2fdde93bac09

📥 Commits

Reviewing files that changed from the base of the PR and between 09c028f and 2ac1f83.

📒 Files selected for processing (5)
  • leap0/_schemas/sandbox.py
  • leap0/models/sandbox.py
  • tests/_async/test_sandboxes.py
  • tests/_sync/test_sandboxes.py
  • tests/models/test_sandbox.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/models/test_sandbox.py
  • leap0/_schemas/sandbox.py

Comment thread leap0/models/sandbox.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
leap0/_sync/ssh.py (1)

12-22: ⚠️ Potential issue | 🟡 Minor

Use ssh_command in the docstring example.

SshAccess exposes ssh_command, not command, so this example currently raises AttributeError. The same typo is mirrored in leap0/_async/ssh.py.

📝 Proposed fix
-            print(access.command)
+            print(access.ssh_command)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@leap0/_sync/ssh.py` around lines 12 - 22, The docstring example references a
non-existent attribute `command` and should use `ssh_command` exposed by the
SshAccess object; update the example in the SshAccess docstrings (both
synchronous and asynchronous modules where present) to call `ssh_command` (e.g.,
replace `access.command` with `access.ssh_command`) so the example does not
raise AttributeError and reflects the actual API surface of SshAccess.
🧹 Nitpick comments (1)
tests/_sync/test_ssh.py (1)

16-19: Add coverage for the new validate/regenerate request shapes.

This only exercises delete_access, but the riskier SSH changes are in validate_access and regenerate_access: both URL templates changed, and validate_access also changed its JSON body. Please add one sync and one async assertion for /ssh/{id}/validate and /ssh/{id}/regen so those paths cannot regress unnoticed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/_sync/test_ssh.py` around lines 16 - 19, Add assertions covering the
new validate and regenerate request shapes: in tests/_sync/test_ssh.py extend
the test (or add a new one) to call
SshClient(mock_transport).validate_access("sbx-1", id="ssh-1", body=...) and
SshClient(mock_transport).regenerate_access("sbx-1", id="ssh-1") and assert
mock_transport.request was called with the correct method+path ("POST",
"/v1/sandbox/sbx-1/ssh/ssh-1/validate") and ("POST",
"/v1/sandbox/sbx-1/ssh/ssh-1/regen"); for validate_access also assert the JSON
body in mock_transport.request.call_args (e.g., call_args[1]['json']) matches
the new expected payload. Mirror those checks in the async test file
(tests/_async/test_ssh.py) by awaiting AsyncSshClient(...).validate_access and
.regenerate_access and asserting the same mock_transport.request call_args for
method, path and validate JSON body so both sync and async paths are covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@leap0/_sync/ssh.py`:
- Line 49: Rename the shadowing parameter named id to a more explicit name
(e.g., access_id) in the SSH client methods to satisfy Ruff A002 and match the
project naming pattern; update the parameter name and its type hint in the
method signatures (e.g., delete_access, get_access, and the other access-related
methods in leap0/_sync/ssh.py and the matching ones in leap0/_async/ssh.py),
update all uses inside each method body, docstrings, and any callers/tests to
use access_id, and ensure both sync and async versions keep identical parameter
names for API symmetry.

In `@tests/models/test_sandbox.py`:
- Line 115: The test's pytest.raises invocation uses a normal string for the
regex match which triggers Ruff RUF043; update the match pattern in the with
pytest.raises(ValueError, match="invalid 'port'.*0") call to use a raw string
literal (e.g., r"invalid 'port'.*0") so the backslashes/regex metacharacters are
not treated as escape sequences and Ruff stops warning; locate the assertion in
tests/models/test_sandbox.py within the test function containing the with
pytest.raises(...) block and replace the match argument with a raw string.
- Around line 79-84: The test test_rejects_invalid_values expects ValueError but
Pydantic v2 wraps errors raised in `@model_validator`(mode="after") into
pydantic.ValidationError; update the test to assert
pytest.raises(ValidationError, ...) for both CreatePresignedURLParams
constructions (and add/import pydantic.ValidationError) so the invalid port and
expires_in cases correctly expect the Pydantic ValidationError.

---

Outside diff comments:
In `@leap0/_sync/ssh.py`:
- Around line 12-22: The docstring example references a non-existent attribute
`command` and should use `ssh_command` exposed by the SshAccess object; update
the example in the SshAccess docstrings (both synchronous and asynchronous
modules where present) to call `ssh_command` (e.g., replace `access.command`
with `access.ssh_command`) so the example does not raise AttributeError and
reflects the actual API surface of SshAccess.

---

Nitpick comments:
In `@tests/_sync/test_ssh.py`:
- Around line 16-19: Add assertions covering the new validate and regenerate
request shapes: in tests/_sync/test_ssh.py extend the test (or add a new one) to
call SshClient(mock_transport).validate_access("sbx-1", id="ssh-1", body=...)
and SshClient(mock_transport).regenerate_access("sbx-1", id="ssh-1") and assert
mock_transport.request was called with the correct method+path ("POST",
"/v1/sandbox/sbx-1/ssh/ssh-1/validate") and ("POST",
"/v1/sandbox/sbx-1/ssh/ssh-1/regen"); for validate_access also assert the JSON
body in mock_transport.request.call_args (e.g., call_args[1]['json']) matches
the new expected payload. Mirror those checks in the async test file
(tests/_async/test_ssh.py) by awaiting AsyncSshClient(...).validate_access and
.regenerate_access and asserting the same mock_transport.request call_args for
method, path and validate JSON body so both sync and async paths are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8d55f0e-0b7e-4b2b-ab6e-ce66509b90e1

📥 Commits

Reviewing files that changed from the base of the PR and between 2ac1f83 and 4b912fc.

📒 Files selected for processing (7)
  • examples/ssh.py
  • leap0/_async/ssh.py
  • leap0/_sync/ssh.py
  • leap0/models/sandbox.py
  • tests/_async/test_ssh.py
  • tests/_sync/test_ssh.py
  • tests/models/test_sandbox.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • leap0/models/sandbox.py

Comment thread leap0/_sync/ssh.py
@intercept_errors("Failed to delete SSH access: ")
def delete_access(self, sandbox: SandboxRef, http_timeout: float | None = None) -> None:
"""Revoke SSH access for a sandbox. The credential is invalidated immediately.
def delete_access(self, sandbox: SandboxRef, *, id: str, http_timeout: float | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Ruff config =="
fd -HI '^(pyproject\.toml|ruff\.toml|\.ruff\.toml|setup\.cfg)$' -x sh -c '
  file="$1"
  echo "--- $file ---"
  rg -n -C2 "A002|per-file-ignores|ignore|select" "$file" || true
' sh {}

echo
echo "== SSH methods using `id` as a parameter =="
rg -nP --type=py '\b(?:async\s+def|def)\s+(delete_access|validate_access|regenerate_access)\([^)]*\bid\s*:' leap0

Repository: leap0-dev/leap0-python

Length of output: 1512


🏁 Script executed:

#!/bin/bash
# Check for API documentation or interface definitions
echo "== Checking for interface/protocol definitions =="
rg -n "class SshAccess|class.*Access|protocol.*Access" leap0 -A 5 | head -50

echo
echo "== Checking similar methods in related classes =="
rg -n "def (delete|validate|regenerate)_" leap0 -B 2 -A 2 --type=py | head -80

echo
echo "== Looking for any inline suppression comments or docstrings =="
grep -n "noqa\|type: ignore\|A002" leap0/_sync/ssh.py leap0/_async/ssh.py || echo "No inline suppressions found"

Repository: leap0-dev/leap0-python

Length of output: 6284


Rename id parameter to a more explicit name like access_id in SSH client methods.

Three methods in both sync and async SSH clients expose id as a parameter, which Ruff flags as A002 (builtin shadowing). This contradicts the codebase pattern where similar resource methods use explicit names (presigned_url_id, recording_id). Rename to access_id or credential_id, or add a targeted suppression if API symmetry is required.

Applies to:

  • leap0/_sync/ssh.py: lines 49, 60, 81
  • leap0/_async/ssh.py: lines 49, 60, 81
🧰 Tools
🪛 Ruff (0.15.9)

[error] 49-49: Function argument id is shadowing a Python builtin

(A002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@leap0/_sync/ssh.py` at line 49, Rename the shadowing parameter named id to a
more explicit name (e.g., access_id) in the SSH client methods to satisfy Ruff
A002 and match the project naming pattern; update the parameter name and its
type hint in the method signatures (e.g., delete_access, get_access, and the
other access-related methods in leap0/_sync/ssh.py and the matching ones in
leap0/_async/ssh.py), update all uses inside each method body, docstrings, and
any callers/tests to use access_id, and ensure both sync and async versions keep
identical parameter names for API symmetry.

Comment thread tests/models/test_sandbox.py
Comment thread tests/models/test_sandbox.py Outdated
@steven-passynkov steven-passynkov merged commit 38551da into main Apr 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant